#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  rule_engine/builtins.py
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are
#  met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following disclaimer
#    in the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the project nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
#  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import collections
import collections.abc
import datetime
import decimal
import functools
import math
import random

from . import ast
from . import errors
from . import types
from .parser.utilities import parse_datetime, parse_float, parse_timedelta

import dateutil.tz

def _builtin_filter(function, iterable):
	return tuple(filter(function, iterable))

def _builtin_map(function, iterable):
	return tuple(map(function, iterable))

def _builtin_parse_datetime(builtins, string):
	return parse_datetime(string, builtins.timezone)

def _builtin_random(boundary=None):
	if boundary:
		if not types.is_natural_number(boundary):
			raise errors.FunctionCallError('argument #1 (boundary) must be a natural number')
		return random.randint(0, int(boundary))
	return random.random()

def _builtin_range(start, stop=None, step=None):
	if not types.is_integer_number(start):
		raise errors.FunctionCallError('argument #1 (start) must be an integer number')
	if stop:
		if not types.is_integer_number(stop):
			raise errors.FunctionCallError('argument #2 (stop) must be an integer number')
		if step:
			if not types.is_integer_number(step):
				raise errors.FunctionCallError('argument #3 (step) must be an integer number')
			return list(range(int(start), int(stop), int(step)))
		return list(range(int(start), int(stop)))
	return list(range(int(start)))

def _builtins_split(string, sep=None, maxsplit=None):
	if maxsplit is None:
		maxsplit = -1
	elif types.is_natural_number(maxsplit):
		maxsplit = int(maxsplit)
	else:
		raise errors.FunctionCallError('argument #3 (maxsplit) must be a natural number')
	return tuple(string.split(sep=sep, maxsplit=maxsplit))

class BuiltinValueGenerator(object):
	"""
	A class used as a wrapper for builtin values to differentiate between a value that is a function and a value that
	should be generated by calling a function. A value that is generated by calling a function is useful for determining
	the value during evaluation for things like the current time.

	.. versionadded:: 4.0.0
	"""
	__slots__ = ('callable',)
	def __init__(self, callable):
		self.callable = callable

	def __call__(self, builtins):
		return self.callable(builtins)

class Builtins(collections.abc.Mapping):
	"""
	A class to define and provide variables to within the builtin context of rules. These can be accessed by specifying
	a symbol name with the ``$`` prefix.
	"""
	scope_name = 'built-in'
	"""The identity name of the scope for builtin symbols."""
	def __init__(self, values, namespace=None, timezone=None, value_types=None):
		"""
		:param dict values: A mapping of string keys to be used as symbol names with values of either Python literals or
			a function which will be called when the symbol is accessed. When using a function, it will be passed a
			single argument, which is the instance of :py:class:`Builtins`.
		:param str namespace: The namespace of the variables to resolve.
		:param timezone: A timezone to use when resolving timestamps.
		:type timezone: :py:class:`~datetime.tzinfo`
		:param dict value_types: A mapping of the values to their datatypes.

		.. versionchanged:: 2.3.0
			Added the *value_types* parameter.
		"""
		self.__values = values
		self.__value_types = value_types or {}
		self.namespace = namespace
		self.timezone = timezone or dateutil.tz.tzlocal()

	def resolve_type(self, name):
		"""
		The method to use for resolving the data type of a builtin symbol.

		:param str name: The name of the symbol to retrieve the data type of.
		:return: The data type of the symbol or :py:attr:`~rule_engine.ast.DataType.UNDEFINED`.
		"""
		return self.__value_types.get(name, ast.DataType.UNDEFINED)

	def __repr__(self):
		return "<{} namespace={!r} keys={!r} timezone={!r} >".format(self.__class__.__name__, self.namespace, tuple(self.keys()), self.timezone)

	def __getitem__(self, name):
		value = self.__values[name]
		if isinstance(value, collections.abc.Mapping):
			if self.namespace is None:
				namespace = name
			else:
				namespace = self.namespace + '.' + name
			return self.__class__(value, namespace=namespace, timezone=self.timezone)
		elif callable(value) and isinstance(value, BuiltinValueGenerator):
			value = value(self)
		return value

	def __iter__(self):
		return iter(self.__values)

	def __len__(self):
		return len(self.__values)

	@classmethod
	def from_defaults(cls, values=None, **kwargs):
		"""Initialize a :py:class:`Builtins` instance with a set of default values."""
		now = BuiltinValueGenerator(lambda builtins: datetime.datetime.now(tz=builtins.timezone))
		# there may be errors here if the decimal.Context precision exceeds what is provided by the math constants
		default_values = {
			# mathematical constants
			'e': decimal.Decimal(repr(math.e)),
			'pi': decimal.Decimal(repr(math.pi)),
			# timestamps
			'now': now,
			'today': BuiltinValueGenerator(lambda builtins: now(builtins).replace(hour=0, minute=0, second=0, microsecond=0)),
			# functions
			'abs': abs,
			'any': any,
			'all': all,
			'sum': sum,
			'map': _builtin_map,
			'max': max,
			'min': min,
			'filter': _builtin_filter,
			'parse_datetime': BuiltinValueGenerator(lambda builtins: functools.partial(_builtin_parse_datetime, builtins)),
			'parse_float': parse_float,
			'parse_timedelta': parse_timedelta,
			'random': _builtin_random,
			'range': _builtin_range,
			'split': _builtins_split
		}
		default_values.update(values or {})
		default_value_types = {
			# mathematical constants
			'e': ast.DataType.FLOAT,
			'pi': ast.DataType.FLOAT,
			# timestamps
			'now': ast.DataType.DATETIME,
			'today': ast.DataType.DATETIME,
			# functions
			'abs': ast.DataType.FUNCTION('abs', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.FLOAT,)),
			'all': ast.DataType.FUNCTION('all', return_type=ast.DataType.BOOLEAN, argument_types=(ast.DataType.ARRAY,)),
			'any': ast.DataType.FUNCTION('any', return_type=ast.DataType.BOOLEAN, argument_types=(ast.DataType.ARRAY,)),
			'sum': ast.DataType.FUNCTION('sum', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.ARRAY(ast.DataType.FLOAT),)),
			'map': ast.DataType.FUNCTION('map', return_type=ast.DataType.ARRAY, argument_types=(ast.DataType.FUNCTION, ast.DataType.ARRAY)),
			'max': ast.DataType.FUNCTION('max', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.ARRAY(ast.DataType.FLOAT),)),
			'min': ast.DataType.FUNCTION('min', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.ARRAY(ast.DataType.FLOAT),)),
			'filter': ast.DataType.FUNCTION('filter', return_type=ast.DataType.ARRAY, argument_types=(ast.DataType.FUNCTION, ast.DataType.ARRAY)),
			'parse_datetime': ast.DataType.FUNCTION('parse_datetime', return_type=ast.DataType.DATETIME, argument_types=(ast.DataType.STRING,)),
			'parse_float': ast.DataType.FUNCTION('parse_float', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.STRING,)),
			'parse_timedelta': ast.DataType.FUNCTION('parse_timedelta', return_type=ast.DataType.TIMEDELTA, argument_types=(ast.DataType.STRING,)),
			'random': ast.DataType.FUNCTION('random', return_type=ast.DataType.FLOAT, argument_types=(ast.DataType.FLOAT,), minimum_arguments=0),
			'range': ast.DataType.FUNCTION('range', return_type=ast.DataType.ARRAY(ast.DataType.FLOAT), argument_types=(ast.DataType.FLOAT, ast.DataType.FLOAT, ast.DataType.FLOAT,), minimum_arguments=1),
			'split': ast.DataType.FUNCTION(
				'split',
				return_type=ast.DataType.ARRAY(ast.DataType.STRING),
				argument_types=(ast.DataType.STRING, ast.DataType.STRING, ast.DataType.FLOAT),
				minimum_arguments=1
			)
		}
		default_value_types.update(kwargs.pop('value_types', {}))
		return cls(default_values, value_types=default_value_types, **kwargs)
